package moodish.comm.component;

import moodish.comm.ClientSideMessage;
import moodish.comm.ServerComm;
import moodish.comm.ServerSideMessage;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 
 * This is the class implementing the ServerComm Interface, and also the
 * Runnable Interface to run a thread waiting for connections.
 * 
 * @author ralso
 * @version 1
 */

public class ServerCommComponent implements ServerComm, Runnable {

	private static final int SERVER_PORT = 4444;
	private ServerSocket serverSocket = null;
	private ConnectedClients connectedClient = null;
	private boolean isStarted = false;
	private BlockingQueue<ServerSideMessage> listOfMessages = new LinkedBlockingQueue<ServerSideMessage>();

	/**
	 * 
	 * Starts the Moodish server. The method should setup a server socket and
	 * begin to accept connections. This method must return immediately after a
	 * server socket has been setup, and another thread should started to listen
	 * for connections.
	 * 
	 */

	public boolean getIsStarted() {
		return isStarted;
	}

	@Override
	public void start() {
		System.out.println("lala");
		try {
			serverSocket = new ServerSocket(SERVER_PORT);
			connectedClient = new ConnectedClients();
			listOfMessages = new LinkedBlockingQueue<ServerSideMessage>();
			isStarted = true;
		} catch (IOException e) {
			e.printStackTrace();
			System.out
					.println("There was a problem creating the server socket");
		}
		new Thread(this).start();
	}

	/**
	 * 
	 * Method used to shutdown the entire server, but before disconnects all
	 * clients connected to him.
	 * 
	 * @throws IOException
	 */

	public void shutdown() throws IOException {
		if (!connectedClient.clients.isEmpty()) {
			List<String> toDelete = new LinkedList<String>();

			for (String client : connectedClient.clients.keySet()) {
				toDelete.add(client);
			}
			for (String client : toDelete) {
				connectedClient.clients.get(client).disconnect();
			}
		}
		serverSocket.close();

	}

	/**
	 * Get the next message received from one of the clients. If no message is
	 * has been received, the method blocks until a message has been received.
	 **/
	@Override
	public ServerSideMessage getNextMessage() {
		ServerSideMessage msg = null;
		if (serverSocket != null && !serverSocket.isClosed()) {
			try {
				msg = listOfMessages.take();
			} catch (InterruptedException e) {
				System.out
						.println("It was thrown an InterruptedException, waiting for a message become available at the list of messages");
			}
		} else {
			System.out
					.println("Its not possible to get the next message since the server socket its not connected or there was a problem instantiating the whole ( messages of all clients ) messages list");
		}

		return msg;

	}

	/**
	 * Checks if a message from a client is pending. If hasNextMessage() returns
	 * true, a call to getNextMessage() will return immediately with the oldest
	 * message in the queue. If hasNextMessage() returns false, a call to
	 * getNextMessage() will block until a message has been received.
	 */

	@Override
	public boolean hasNextMessage() {
		if (listOfMessages.isEmpty())
			return false;
		return true;
	}

	/**
	 * Send a message to a client. This method should be used to relay messages
	 * received from a client to a friendship. This method should be called once
	 * for each friendship.
	 */

	@Override
	public void sendMoodishMessage(String fromNickname, String toNickname,
			String moodishMessage) {

		Client clientTo = searchClient(toNickname);
		if (clientTo != null && !clientTo.getClientSocket().isClosed()) {
			clientTo.send(moodishMessage, fromNickname,
					ClientSideMessage.Type.MOODISH_MESSAGE);
		} else {
			System.out
					.println("There was a problem trying to send a moodish message to this specific client with this nickname, maybe he is not connected or doesnt even exist on the list.");
		}

	}

	/**
	 * Inform a client that another client is now a friendship.
	 */
	@Override
	public void sendNewFriendship(String toNickname, String newFriendship) {

		Client clientTo = searchClient(toNickname);
		if (clientTo != null && !clientTo.getClientSocket().isClosed()) {
			clientTo.send(newFriendship, "SERVER",
					ClientSideMessage.Type.FRIENDSHIP);
		} else {
			System.out
					.println("There was a problem trying to send a friend message to this specific client with this nickname, maybe he is not connected or doesnt even exist on the list.");
		}

	}

	/**
	 * Inform a client that a former friendship has decided to unfollow.
	 * 
	 */

	@Override
	public void sendNewUnfriendship(String toNickname, String unfriendship) {

		Client clientTo = searchClient(toNickname);
		if (clientTo != null && !clientTo.getClientSocket().isClosed()) {
			clientTo.send(unfriendship, "SERVER",
					ClientSideMessage.Type.UNFRIENDSHIP);
		} else {
			System.out
					.println("There was a problem trying to send a unfollow message to this specific client with this nickname, maybe he is not connected or doesnt even exist on the list.");
		}

	}

	/**
	 * Report an error to a client. An error can, for instance, be trying to
	 * follow a client who is not connected, trying to unfollow a client not
	 * currently followed, and so on.
	 */
	@Override
	public void sendError(String toNickname, String error) {

		Client clientTo = searchClient(toNickname);
		if (clientTo != null && !clientTo.getClientSocket().isClosed()) {
			clientTo.send(error, "SERVER", ClientSideMessage.Type.ERROR);
		} else {
			System.out
					.println("There was a problem trying to send a error message to this specific client with this nickname, maybe he is not connected or doesnt even exist on the list.");
		}

	}

	/**
	 * 
	 * Inform a client that a new client has connected. When a new client
	 * connects, the server should call this method for each client already
	 * connected to inform the newly connected client about the clients
	 * currently online.
	 * 
	 */

	@Override
	public void sendClientConnected(String toNickname, String connectedNickname) {

		Client clientTo = searchClient(toNickname);
		if (clientTo != null && !clientTo.getClientSocket().isClosed()) {
			clientTo.send(connectedNickname, "SERVER",
					ClientSideMessage.Type.CONNECTED);
		} else {
			System.out
					.println("There was a problem trying to send a client connected message to this specific client with this nickname, maybe he is not connected or doesnt even exist on the list.");
		}

	}

	/**
	 * Inform a client that another client disconnected.
	 */

	@Override
	public void sendClientDisconnected(String toNickname,
			String disconnectedNickname) {

		Client clientTo = searchClient(toNickname);
		if (clientTo != null && !clientTo.getClientSocket().isClosed()) {
			clientTo.send(disconnectedNickname, "SERVER",
					ClientSideMessage.Type.DISCONNECTED);
		} else {
			System.out
					.println("There was a problem trying to send a client disconnected message to this specific client with this nickname, maybe he is not connected or doesnt even exist on the list.");
		}

	}

	/**
	 * Checks if a client with a certain nickname is currently connected.
	 */

	@Override
	public boolean clientIsConnected(String nickname) {

		Client isHeConnected = searchClient(nickname);
		return isHeConnected != null && isHeConnected.getClientSocket() != null
				&& !isHeConnected.getClientSocket().isClosed();
	}

	/**
	 * Disconnect a client.
	 */

	@Override
	public void disconnectClient(String nickname) {
		if (serverSocket != null && !serverSocket.isClosed()) {
			Client toBeDisconnected = searchClient(nickname);
			if (toBeDisconnected != null
					&& !toBeDisconnected.getClientSocket().isClosed()) {
				toBeDisconnected.disconnect();
			} else {
				System.out
						.println("There was a problem trying to disconnect this specific client with this nickname, maybe he is not connected or it doesnt even exist on the list, or maybe his socket was already closed");
			}
		} else {
			System.out
					.println("Can't disconnect any user since the server is not even online");
		}

	}

	/**
	 * 
	 * Search a client from the list of clients by his nickname. An object from
	 * the class client contains the socket associated for each client
	 * 
	 * @param nickname
	 * @return Client
	 */

	private Client searchClient(String nickname) {

		return connectedClient.searchClient(nickname);
	}

	@Override
	public void run() {
		while (!serverSocket.isClosed()) {
			try {
				Socket socketOfNewClientArrived = serverSocket.accept();

				ObjectInputStream inputOfNewClient = new ObjectInputStream(
						socketOfNewClientArrived.getInputStream());
				ObjectOutputStream outputOfNewClient = new ObjectOutputStream(
						socketOfNewClientArrived.getOutputStream());
				new Thread(new Client(socketOfNewClientArrived,
						inputOfNewClient, outputOfNewClient)).start();

			} catch (IOException e) {
				System.out
						.println("There was a problem waiting for a specific client, server socket will be force closed");

			}

		}

	}

	/**
	 * Inner class - Client Each client has an associated socket,
	 * ObjectOutputStream and ObjectInputStream. Used to contact with other
	 * people
	 */

	class Client implements Runnable {

		private String nickname;
		private Socket clientSocket;

		private ObjectOutputStream out;
		private ObjectInputStream in;

		private Client(Socket clientSocket, ObjectInputStream in,
				ObjectOutputStream out) {
			this.nickname = null;
			this.clientSocket = clientSocket;
			this.in = in;
			this.out = out;

		}

		/**
		 * Method used to disconnect a specific client from the server socket,
		 * removing it also from the Clients List.
		 */

		private void disconnect() {

			try {
				clientSocket.close();
				connectedClient.remove(nickname);

			} catch (IOException e) {
				System.out
						.println("An IOException was thrown closing a socket because of some Thread Waiting on a I/O operation");
				;
			}

		}

		/**
		 * Method used to send ClientSideMessage throw some ObjectOutputStream
		 * of a specific client.
		 * 
		 * @param msg
		 * @param from
		 * @param type
		 */

		private void send(String payload, String from,
				ClientSideMessage.Type type) {
			if (out != null && !clientSocket.isClosed()) {
				try {
					out.writeObject(new ClientSideMessageComponent(from,
							payload, type));
				} catch (IOException e) {
					System.out
							.println("There was a problem sending message to a specific client--AIAI");
				}
			} else {
				System.out
						.println("There was a problem trying to send the message from a specific client, maybe the ObjectOutputStream is not instantiated or the socket was already been closed.");
			}

		}

		/**
		 * Method get of clientSocket to check later if its connected.
		 * 
		 * @return
		 */

		private Socket getClientSocket() {
			return clientSocket;
		}

		@Override
		public void run() {

			try {
				ServerSideMessage nicknameMsg = (ServerSideMessage) in
						.readObject();
				if (nicknameMsg.getType() == ServerSideMessage.Type.CLIENT_CONNECTED) {
					this.nickname = nicknameMsg.getClientNickname();
					connectedClient.addOut(nickname, this);
					try {
						listOfMessages.put(nicknameMsg);
					} catch (InterruptedException e) {
						System.out
								.println("There was a problem waiting on the method put of the BlockingQueue");
					}

				}

				try {
					while (!clientSocket.isClosed()) {
						ServerSideMessage msgReceived = (ServerSideMessage) in
								.readObject();
						listOfMessages.put(msgReceived);
					}

				} catch (ClassNotFoundException e) {
					System.out
							.println("There was a problem casting the object readed to a ServerSideMessage instance");
				} catch (IOException e) {
					System.out
							.println("There was a problem with some client specific socket, "
									+ nickname + " has disconnected");
					try {
						listOfMessages.put(new ServerSideMessageComponent(
								nickname, null,
								ServerSideMessage.Type.CLIENT_DISCONNECTED));
					} catch (InterruptedException e1) {
						System.out
								.println("There was a problem waiting on the method put of the BlockingQueue to put the client disconnected message");
					}
				} catch (InterruptedException e) {
					System.out
							.println("There was a problem waiting on the method put of the BlockingQueue");
				}

			} catch (ClassNotFoundException e1) {
				System.out
						.println("There was a problem casting the object readed to a ServerSideMessage instance");
			} catch (IOException e1) {
				System.out
						.println("There was a problem with some client specific socket, maybe the socket as been closed");
			}

		}
	}

	/**
	 * 
	 * Synchronized Map of clients connected to the server
	 * 
	 */
	class ConnectedClients {

		private Map<String, Client> clients = new HashMap<String, Client>();

		/**
		 * Method synchronized to add a new client to the list of clients
		 * connected to the server.
		 * 
		 * @param String
		 *            name
		 * @param Client
		 *            client
		 */
		private synchronized void addOut(String name, Client c) {
			clients.put(name, c);
		}

		/**
		 * Method synchronized to remove a client to the list of clients
		 * connected to the server.
		 * 
		 * @param String
		 *            name
		 * @throws IOException
		 */

		private synchronized void remove(String name) throws IOException {
			clients.remove(name);
		}

		/**
		 * Method synchronized to get a specific client from the list of clients
		 * connected to the server by his nickname.
		 * 
		 * @param String
		 *            name
		 * @return Client client
		 */
		private synchronized Client searchClient(String name) {
			return clients.get(name);
		}

	}

}
